package com.sakiko.audiobook.search.service.impl;

import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch._types.FieldValue;
import co.elastic.clients.elasticsearch._types.SortOrder;
import co.elastic.clients.elasticsearch._types.aggregations.Aggregate;
import co.elastic.clients.elasticsearch._types.query_dsl.BoolQuery;
import co.elastic.clients.elasticsearch.core.SearchRequest;
import co.elastic.clients.elasticsearch.core.SearchResponse;
import co.elastic.clients.elasticsearch.core.search.Hit;
import co.elastic.clients.elasticsearch.core.search.Suggestion;
import com.alibaba.fastjson.JSON;
import com.sakiko.audiobook.album.AlbumInfoFeignClient;
import com.sakiko.audiobook.album.CategoryFeignClient;
import com.sakiko.audiobook.common.constant.RedisConstant;
import com.sakiko.audiobook.common.execption.SakiException;
import com.sakiko.audiobook.common.result.Result;
import com.sakiko.audiobook.common.util.PinYinUtils;
import com.sakiko.audiobook.model.album.*;
import com.sakiko.audiobook.model.search.AlbumInfoIndex;
import com.sakiko.audiobook.model.search.AttributeValueIndex;
import com.sakiko.audiobook.model.search.SuggestIndex;
import com.sakiko.audiobook.query.search.AlbumIndexQuery;
import com.sakiko.audiobook.search.repository.AlbumIndexRepository;
import com.sakiko.audiobook.search.repository.SuggestIndexRepository;
import com.sakiko.audiobook.search.service.SearchService;
import com.sakiko.audiobook.user.client.UserInfoFeignClient;
import com.sakiko.audiobook.vo.search.AlbumInfoIndexVo;
import com.sakiko.audiobook.vo.search.AlbumSearchResponseVo;
import com.sakiko.audiobook.vo.user.UserInfoVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.elasticsearch.core.suggest.Completion;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import java.io.IOException;
import java.sql.Array;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.stream.Collectors;


@Slf4j
@Service
@SuppressWarnings({"all"})
public class SearchServiceImpl implements SearchService {

    // 如何利用 es客户端实现保存数据到 es!
    @Autowired
    private AlbumInfoFeignClient albumInfoFeignClient;

    @Autowired
    private CategoryFeignClient categoryFeignClient;

    @Autowired
    private UserInfoFeignClient userInfoFeignClient;

    @Autowired
    private AlbumIndexRepository albumIndexRepository;

    // 自定义线程池
    @Autowired
    private ThreadPoolExecutor threadPoolExecutor;

    @Autowired
    private ElasticsearchClient elasticsearchClient;

    @Autowired
    private SuggestIndexRepository suggestIndexRepository;

    @Autowired
    private RedisTemplate redisTemplate;

    @Override
    public void upperAlbum(Long albumId) {
        // 给 AlbumInfoIndex对象赋值，然后将其保存到索引库！
        AlbumInfoIndex albumInfoIndex = new AlbumInfoIndex();
        // 赋值 albumInfoIndex属性值来自不同的表; 表都在哪？--> tingshu_album数据库下 --> 专辑微服务下
        CompletableFuture<AlbumInfo> albumInfoCompletableFuture = CompletableFuture.supplyAsync(() -> {
            // 远程调用
            Result<AlbumInfo> albumInfoResult = albumInfoFeignClient.getAlbumInfo(albumId);
            Assert.notNull(albumInfoResult, "专辑结果集不存在！");
            AlbumInfo albumInfo = albumInfoResult.getData();
            Assert.notNull(albumInfo, "专辑不存在！");
            // 属性拷贝 (albumInfo)
            BeanUtils.copyProperties(albumInfo, albumInfoIndex);
            // 获取属性与属性值信息; 赋值; 远程调用获取数据; album_attribute_value
            List<AlbumAttributeValue> albumAttributeValueVoList = albumInfo.getAlbumAttributeValueVoList();
            // 声明一个集合
            ArrayList<AttributeValueIndex> attributeValueIndexArrayList = new ArrayList<>();
            // 循环遍历获取属性Id与属性值Id并赋值
            for (AlbumAttributeValue albumAttributeValue : albumAttributeValueVoList) {
                // 创建对象
                AttributeValueIndex attributeValueIndex = new AttributeValueIndex();
                // 赋值
                attributeValueIndex.setAttributeId(albumAttributeValue.getAttributeId());
                attributeValueIndex.setValueId(albumAttributeValue.getValueId());
                attributeValueIndexArrayList.add(attributeValueIndex);
            }
            // 赋值数据
            albumInfoIndex.setAttributeValueIndexList(attributeValueIndexArrayList);
            return albumInfo;
        }, threadPoolExecutor).exceptionally(throwable -> {
            log.error("远程调用失败！" + throwable.getMessage());
            return null;
        });

        // (category*Id) 通过视图取更方便 -> 视图中的 id对应三级分类 id -> 根据 albumInfo中的 3id去找视图
        CompletableFuture<Boolean> categoryCompletableFuture = albumInfoCompletableFuture.thenApplyAsync(albumInfo -> {
            // 远程调用 (虽然这里不需要返回值，本来应当使用 .thenAcceptAsync() 但是判断远程调用是否成功时不好判断故更改为 .thenApplyAsync() )
            Result<BaseCategoryView> baseCategoryViewResult = categoryFeignClient.getBaseCategoryViewByCategoryId(albumInfo.getCategory3Id());
            Assert.notNull(baseCategoryViewResult, "视图结果集不存在");
            BaseCategoryView baseCategoryView = baseCategoryViewResult.getData();
            Assert.notNull(baseCategoryView, "视图不存在");
            albumInfoIndex.setCategory1Id(baseCategoryView.getCategory1Id());
            albumInfoIndex.setCategory2Id(baseCategoryView.getCategory2Id());
            return true;
        }, threadPoolExecutor).exceptionally(throwable -> {
            log.error("远程调用失败！" + throwable.getMessage());
            return false;
        });

        // 获取用户信息
        CompletableFuture<Boolean> userCompletableFuture = albumInfoCompletableFuture.thenApplyAsync(albumInfo -> {
            Result<UserInfoVo> userInfoVoResult = userInfoFeignClient.getUserInfo(albumInfo.getUserId());
            Assert.notNull(userInfoVoResult, "用户信息结果集不存在！");
            UserInfoVo userInfoVo = userInfoVoResult.getData();
            Assert.notNull(userInfoVo, "用户信息不存在！");
            albumInfoIndex.setAnnouncerName(userInfoVo.getNickname());
            // 专辑统计信息; 根据专辑 Id查询专辑的统计信息; album_stat
            albumInfoIndex.setPlayStatNum(new Random().nextInt(1000));
            albumInfoIndex.setCommentStatNum(new Random().nextInt(100));
            albumInfoIndex.setSubscribeStatNum(new Random().nextInt(100000));
            albumInfoIndex.setBuyStatNum(new Random().nextInt(100000));
            albumInfoIndex.setHotScore(new Random().nextDouble(1000));
            return true;
        }, threadPoolExecutor).exceptionally(throwable -> {
            log.error("远程调用失败！" + throwable.getMessage());
            return false;
        });

        // 异常处理
        try {
            if (null == albumInfoCompletableFuture.get() || !categoryCompletableFuture.get() || !userCompletableFuture.get()) {
                throw new SakiException(808, "上架专辑失败！");
            }
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } catch (ExecutionException e) {
            throw new RuntimeException(e);
        }
        // 多任务组合:
        CompletableFuture.allOf(
                albumInfoCompletableFuture,
                categoryCompletableFuture,
                userCompletableFuture
        ).join();

        // 保存
        albumIndexRepository.save(albumInfoIndex);
        // 上架 suggestinfo提词库; 标题; 简介; 作者
        SuggestIndex suggestIndexTitle = new SuggestIndex();
        suggestIndexTitle.setId(UUID.randomUUID().toString().replaceAll("-", ""));
        suggestIndexTitle.setTitle(albumInfoIndex.getAlbumTitle());
        suggestIndexTitle.setKeyword(new Completion(new String[]{albumInfoIndex.getAlbumTitle()}));
        suggestIndexTitle.setKeywordPinyin(new Completion(new String[]{PinYinUtils.toHanyuPinyin(albumInfoIndex.getAlbumTitle())}));
        suggestIndexTitle.setKeywordSequence(new Completion(new String[]{PinYinUtils.getFirstLetter(albumInfoIndex.getAlbumTitle())}));
        suggestIndexRepository.save(suggestIndexTitle);
        // 专辑 (测试后好像专辑的自动补全有点多，之后上线测试等可以重新批量上架一次，更新掉suggestinfo索引库中关于专辑的这部分)
//        SuggestIndex suggestIndexAlbumIntro = new SuggestIndex();
//        suggestIndexAlbumIntro.setId(UUID.randomUUID().toString().replaceAll("-", ""));
//        suggestIndexAlbumIntro.setTitle(albumInfoIndex.getAlbumIntro());
//        suggestIndexAlbumIntro.setKeyword(new Completion(new String[]{albumInfoIndex.getAlbumIntro()}));
//        suggestIndexAlbumIntro.setKeywordPinyin(new Completion(new String[]{PinYinUtils.toHanyuPinyin(albumInfoIndex.getAlbumIntro())}));
//        suggestIndexAlbumIntro.setKeywordSequence(new Completion(new String[]{PinYinUtils.getFirstLetter(albumInfoIndex.getAlbumIntro())}));
//        suggestIndexRepository.save(suggestIndexAlbumIntro);
        // 作者 (略)
    }

    @Override
    public void lowerAlbum(Long albumId) {
        // 下架
        albumIndexRepository.deleteById(albumId);
    }

    @Override
    public AlbumSearchResponseVo search(AlbumIndexQuery albumIndexQuery) {
        // 构建 DSL语句
        SearchRequest searchRequest = this.queryBuildDSL(albumIndexQuery);
        // 根据关键词检索数据
        SearchResponse<AlbumInfoIndex> searchResponse = null;
        try {
            searchResponse = elasticsearchClient.search(searchRequest, AlbumInfoIndex.class);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        // 创建对象并赋值
        AlbumSearchResponseVo albumSearchResponseVo = this.parseResultData(searchResponse);
        albumSearchResponseVo.setTotal(searchResponse.hits().total().value());
        albumSearchResponseVo.setPageNo(albumIndexQuery.getPageNo());
        albumSearchResponseVo.setPageSize(albumIndexQuery.getPageSize());
        albumSearchResponseVo.setTotalPages((albumSearchResponseVo.getTotal() + albumSearchResponseVo.getPageSize() - 1) / albumSearchResponseVo.getPageSize());
//        albumSearchResponseVo.setTotalPages(albumSearchResponseVo.getTotal() % albumSearchResponseVo.getPageSize() == 0 ? albumSearchResponseVo.getTotal() / albumSearchResponseVo.getPageSize() : albumSearchResponseVo.getTotal() / albumSearchResponseVo.getPageSize() + 1);
        // 返回数据
        return albumSearchResponseVo;
    }

    @Override
    public List<Map<String, Object>> getChannel(Long category1Id) {
        // 获取到一级分类下的三级分类数据集合
        Result<List<BaseCategory3>> baseCategory3ListResult = categoryFeignClient.findTopBaseCategory3(category1Id);
        // 获取数据并判断
        Assert.notNull(baseCategory3ListResult, "一级分类下的三级分类数据集合不存在！");
        List<BaseCategory3> baseCategory3List = baseCategory3ListResult.getData();
        Assert.notNull(baseCategory3List, "一级分类下的三级分类数据不存在！");
        // 将 baseCategory3List转换为 map; key = category3Id; value = category3;
        Map<Long, BaseCategory3> category3Map = baseCategory3List.stream().collect(Collectors.toMap(BaseCategory3::getId, baseCategory3 -> baseCategory3));
        // 通过 baseCategory3List获取三级分类 Id，根据三级分类 Id查询专辑列表
        // es客户端查询数据 terms.value()需要 LIST<FieldValue>而不是 List<Long>
        List<FieldValue> valueList = baseCategory3List.stream().map(category3 -> FieldValue.of(category3.getId())).collect(Collectors.toList());
        SearchResponse<AlbumInfoIndex> searchResponse = null;
        try {
            searchResponse = elasticsearchClient.search(s -> s.index("albuminfo")
                            .query(q -> q.terms(t -> t.field("category3Id").terms(ts -> ts.value(valueList))))
                            .aggregations("groupByCategory3IdAgg", a -> a.terms(ts -> ts.field("category3Id"))
                                    .aggregations("topSixHotScoreAgg", ag -> ag.topHits(th -> th.size(6)
                                            .sort(so -> so.field(f -> f.field("hotScore").order(SortOrder.Desc)))))
                            )
                    , AlbumInfoIndex.class);
        } catch (IOException e) {
            System.out.println("elasticsearchClient查询失败");
            throw new RuntimeException(e);
        }
        // 获取数据
        Aggregate groupByCategory3IdAgg = searchResponse.aggregations().get("groupByCategory3IdAgg");
        List<Map<String, Object>> mapList = groupByCategory3IdAgg.lterms().buckets().array().stream().map(bucket -> {
            // 创建 map集合 key: 三级分类对象; value: 专辑集合数据
            Map<String, Object> map = new HashMap<>();
            // 通过三级分类 Id(索引库 groupByCategory3IdAgg.buckrt.key)获取到三级分类对象
            BaseCategory3 baseCategory3 = category3Map.get(bucket.key());
            // 通过 source数据，得到聚合 agg、排序 sort(规则见上面的 esClient)后的 专辑数据 【并收集成 list】
            List<AlbumInfoIndex> albumInfoIndexList = bucket.aggregations().get("topSixHotScoreAgg").topHits().hits().hits().stream().map(hit ->
                    JSON.parseObject(hit.source().toString(), AlbumInfoIndex.class)).collect(Collectors.toList());
            // 返回数据
            map.put("baseCategory3", baseCategory3);
            map.put("list", albumInfoIndexList);
            return map;
        }).collect(Collectors.toList());
        return mapList;
    }

    @Override
    public List<String> completeSuggest(String keyword) {
        // 编写 DSL语句
        SearchResponse<SuggestIndex> searchResponse = null;
        try {
            searchResponse = elasticsearchClient.search(s -> s.index("suggestinfo")
                            .suggest(su -> su.suggesters("keyword-suggest", ks -> ks.prefix(keyword)
                                            .completion(c -> c.field("keyword")
                                                    .skipDuplicates(true)
                                                    .fuzzy(f -> f.fuzziness("auto"))
                                                    .size(10)
                                            ))
                                    .suggesters("keywordPinyin-suggest", ks -> ks.prefix(keyword)
                                            .completion(c -> c.field("keywordPinyin")
                                                    .skipDuplicates(true)
                                                    .fuzzy(f -> f.fuzziness("auto"))
                                                    .size(10)
                                            ))
                                    .suggesters("keywordSequence-suggest", ks -> ks.prefix(keyword)
                                            .completion(c -> c.field("keywordSequence")
                                                    .skipDuplicates(true)
                                                    .fuzzy(f -> f.fuzziness("auto"))
                                                    .size(10)
                                            ))
                            )
                    , SuggestIndex.class);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        // 获取执行结果 (根据查询search时自定义的名称获取) ==> searchResponse.suggest().get("keyword-suggest");
        List<String> list = new ArrayList<>();
        list.addAll(this.getCommonSuggest(searchResponse, "keyword-suggest"));
        list.addAll(this.getCommonSuggest(searchResponse, "keywordPinyin-suggest"));
        list.addAll(this.getCommonSuggest(searchResponse, "keywordSequence-suggest"));
        return list.stream().distinct().collect(Collectors.toList()); // 去重下
    }

    @Override
    public void updateLatelyAlbumRanking() {
        // 通过远程调用获取所有一级分类 Id
        Result<List<BaseCategory1>> baseCategory1ListResult = categoryFeignClient.findAllCategory1();
        Assert.notNull(baseCategory1ListResult, "一级分类集合结果集为空");
        List<BaseCategory1> baseCategory1List = baseCategory1ListResult.getData();
        for (BaseCategory1 baseCategory1 : baseCategory1List) {
            // 每个一级分类Id下有五个维度; 热度、播放量、订阅量、购买量、评论数
            String[] rankingDimensionArray = new String[]{"hotScore", "playStatNum", "subscribeStatNum", "buyStatNum", "commentStatNum"};
            for (String rankingDimension : rankingDimensionArray) {
                // 1、通过 DSL语句查询到数据结果集 (每个一级分类 Id下的 上面五种属性的排序)
                SearchResponse<AlbumInfoIndex> searchResponse = null;
                try {
                    searchResponse = elasticsearchClient.search(s -> s.index("albuminfo")
                                    .query(q -> q.term(t -> t.field("category1Id").value(baseCategory1.getId())))
                                    .sort(so -> so.field(fi -> fi.field(rankingDimension).order(SortOrder.Desc)))
                            , AlbumInfoIndex.class);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
                // 获取排行榜某一 rankingDimension集合数据
                List<AlbumInfoIndex> albumInfoIndexList = searchResponse.hits().hits().stream().map(Hit::source).collect(Collectors.toList());
                // 2、将结果集存储到 redis中 key field value  --> hset key field value
                this.redisTemplate.opsForHash().put(RedisConstant.RANKING_KEY_PREFIX + baseCategory1.getId(), rankingDimension, albumInfoIndexList);
            }
        }
    }

    @Override
    public List<AlbumInfoIndex> findRankingList(Long category1Id, String rankingDimension) {
        // hget key field; ==> value: [albumInfoIndexList]
        return (List<AlbumInfoIndex>) redisTemplate.opsForHash().get(RedisConstant.RANKING_KEY_PREFIX + category1Id, rankingDimension);
    }

    // todo: 获取提词结果集
    private Collection<String> getCommonSuggest(SearchResponse<SuggestIndex> searchResponse, String keyword) {
        List<Suggestion<SuggestIndex>> suggestions = searchResponse.suggest().get(keyword);
        List<String> list = suggestions.get(0).completion().options().stream().map(option ->
                option.source().getTitle()).collect(Collectors.toList());
        return list;
    }

    // todo: searchRequest 承载 DSL语句
    private SearchRequest queryBuildDSL(AlbumIndexQuery albumIndexQuery) {
        // SearchRequest对象构造器私有！！！ 但是该类有提供 builder().build()
        SearchRequest.Builder searchRequestBuilder = new SearchRequest.Builder();
        // note: 第二种方式: 声明一个 query对象【使用 BoolQuery承接多条件查询】
        BoolQuery.Builder boolQueryBuilder = new BoolQuery.Builder();
        // 设置索引库 index -> albuminfo
        searchRequestBuilder.index("albuminfo");
        // a、根据关键词检索
        if (!StringUtils.isEmpty(albumIndexQuery.getKeyword())) {
            // 根据关键词查询
            // todo: 第一种方式 query --> bool --> must --> match --> ...
//            searchRequestBuilder.query(q -> q.bool(
//                    b -> b.must(
//                            mu -> mu.match(
//                                    ma -> ma.field("albumTitle").query(albumIndexQuery.getKeyword())
//                            )
//                    )
//            ));
            // todo: 第二种方式 bool --> must --> match
            // boolQueryBuilder.must(mu -> mu.match(ma -> ma.field("albumTitle").query(albumIndexQuery.getKeyword())));
            boolQueryBuilder.should(s -> s.match(m -> m.field("albumTitle").query(albumIndexQuery.getKeyword())))
                    .should(s -> s.match(m -> m.field("albumIntro").query(albumIndexQuery.getKeyword())))
                    .should(s -> s.match(m -> m.field("announcerName").query(albumIndexQuery.getKeyword())));
            // 设置高亮
            searchRequestBuilder.highlight(h -> h.fields(
                    "albumTitle",
                    f -> f.preTags("<span style=color:red>").postTags("</span>")
            ));
        }
        // b、根据分类Id检索
        if (!StringUtils.isEmpty(albumIndexQuery.getCategory1Id())) {
            // bool --> filter --> term [这里各个 term之间是 and关系]
            boolQueryBuilder.filter(f -> f.term(t -> t.field("category1Id").value(albumIndexQuery.getCategory1Id())));
        }
        if (!StringUtils.isEmpty(albumIndexQuery.getCategory2Id())) {
            boolQueryBuilder.filter(f -> f.term(t -> t.field("category2Id").value(albumIndexQuery.getCategory2Id())));
        }
        if (!StringUtils.isEmpty(albumIndexQuery.getCategory3Id())) {
            boolQueryBuilder.filter(f -> f.term(t -> t.field("category3Id").value(albumIndexQuery.getCategory3Id())));
        }
        // 处理属性与属性值过滤: 必然属于第二次以后查询项 【已经通过 关键词检索 或 分类id过滤】
        List<String> attributeList = albumIndexQuery.getAttributeList();
        if (!CollectionUtils.isEmpty(attributeList)) {
            for (String attribute : attributeList) {
                // attribute = <属性 id:属性值 id>
                String[] split = attribute.split(":");
                // 判断数据是否存在; 是否符合标准模式
                if (null != split && split.length == 2) {
                    // 获取到属性id=split[0]; 属性值id=split[1]; 构建 DSL语句: 属性id:属性值id 数据类型 Nested 能够允许数据彼此独立的检索和查询！
                    boolQueryBuilder.filter(f -> f.nested(n -> n.path("attributeValueIndexList")
                            .query(qu -> qu.bool(b -> b.filter(fi -> fi.term(t -> t.field("attributeValueIndexList.attributeId").value(split[0])))
                                    .filter(fi -> fi.term(t -> t.field("attributeValueIndexList.valueId").value(split[1])))
                            ))
                    ));
                }
            }
        }
        // 排序: 综合排序[1:desc] 播放量[2:desc] 发布时间[3:desc]；asc:升序 desc:降序
        String order = albumIndexQuery.getOrder();
        if (!StringUtils.isEmpty(order)) {
            // 设置排序规则
            String[] split = order.split(":");
            if (null != split && split.length == 2) {
                String orderRulerWord = "";
                switch (split[0]) {
                    case "1": // 综合排序
                        orderRulerWord = "hotScore";
//                        searchRequestBuilder.sort(s -> s.field(f -> f.field("hotScore").order("asc".equals(split[1]) ? SortOrder.Asc : SortOrder.Desc)));
                        break;
                    case "2": // 播放量
                        orderRulerWord = "playStatNum";
//                        searchRequestBuilder.sort(s -> s.field(f -> f.field("playStatNum").order("asc".equals(split[1]) ? SortOrder.Asc : SortOrder.Desc)));
                        break;
                    case "3": // 发布时间
                        orderRulerWord = "createTime";
//                        searchRequestBuilder.sort(s -> s.field(f -> f.field("createTime").order("asc".equals(split[1]) ? SortOrder.Asc : SortOrder.Desc)));
                        break;
                }
                // 设置排序规则:
                String finalOrderRulerWord = orderRulerWord;
                searchRequestBuilder.sort(s -> s.field(f -> f.field(finalOrderRulerWord).order("asc".equals(split[1]) ? SortOrder.Asc : SortOrder.Desc)));
            }
        }
        // 设置分页
        int from = (albumIndexQuery.getPageNo() - 1) * albumIndexQuery.getPageSize();
        searchRequestBuilder.from(from);
        searchRequestBuilder.size(albumIndexQuery.getPageSize());
        // query --> bool
        searchRequestBuilder.query(boolQueryBuilder.build()._toQuery());
        // 构建对象
        SearchRequest searchRequest = searchRequestBuilder.build();
        // 测试 打印下 DSL语句
        System.out.println("测试输出 DSL:\t" + searchRequest.toString());
        // 返回 searchRequest
        return searchRequest;
    }

    // todo: 将检索出的结果集进行转换 【主要是成员属性 List<AlbumInfoIndexVo> list的赋值】
    private AlbumSearchResponseVo parseResultData(SearchResponse<AlbumInfoIndex> searchResponse) {
        // 创建对象 albumSearchResponseVo
        AlbumSearchResponseVo albumSearchResponseVo = new AlbumSearchResponseVo();
        // 成员属性 List<AlbumInfoIndexVo> list
        List<AlbumInfoIndexVo> albumInfoIndexList = searchResponse.hits().hits().stream().map(hit -> {
            // 创建 albumInfoIndexVo对象
            AlbumInfoIndexVo albumInfoIndexVo = new AlbumInfoIndexVo();
            // 获取结果集对象
            AlbumInfoIndex albumInfoIndex = hit.source();
            // 属性赋值
            BeanUtils.copyProperties(albumInfoIndex, albumInfoIndexVo);
            // 判断是否有高亮数据显示
            if (null != hit.highlight().get("albumTitle")) {
                // 获取高亮字段
                String albumTitle = hit.highlight().get("albumTitle").get(0);
                albumInfoIndexVo.setAlbumTitle(albumTitle);
            }
            // 返回数据
            return albumInfoIndexVo;
        }).collect(Collectors.toList());
        // 设置返回数据
        albumSearchResponseVo.setList(albumInfoIndexList);
        // 返回
        return albumSearchResponseVo;
    }
}

/**
 * 对应查询的相关笔记 DSL
 * note: queryBuildDSL对应的原 DSL语句
 * -----------------------------
 * 1> 根据关键词查询
 * GET /albuminfo/_search
 * {
 * "query": {
 * "bool": {
 * "must": [
 * {
 * "match": {
 * "albumTitle": "三国"
 * }
 * }
 * ]
 * }
 * },
 * "highlight": {
 * "fields": {
 * "albumTitle": {}
 * },
 * "pre_tags": "<span style=color: red>",
 * "post_tags": "</span>"
 * }
 * }
 * 2> 根据分类Id查询
 * GET /albuminfo/_search
 * {
 * "query": {
 * "bool": {
 * "filter": [
 * {
 * "term": {
 * "category1Id": "11"
 * }
 * },
 * {
 * "term": {
 * "category2Id": "165"
 * }
 * }
 * ]
 * }
 * }
 * }
 * note: 在匹配时，什么时候用 query？什么时候用 value?
 * -----------------------------------------------
 * query和 value的区别, 反映了 Elasticsearch 两种核心的匹配方式: “全文搜索” vs “精确匹配”。
 * todo: match 查询中的 .query() --> 全文搜索 (Full-Text Search)
 * todo: term 查询中的 .value() --> 精确匹配 (Exact Match)
 * 两者不能混用，否则编译就不通过
 * .query() 是为需要分析的查询（如 match, match_phrase, query_string）设计的，
 * 而 .value() 是为不需要分析的查询（如 term, terms, range）设计的。
 * note: 根据属性id和属性值id进行过滤的原 DSL语句
 * ----------------------------------------
 * GET /albuminfo/_search
 * {
 * "query": {
 * "nested": {
 * "path": "attributeValueIndexList",
 * "query": {
 * "bool": {
 * "filter": [
 * {
 * "term": {
 * "attributeValueIndexList.attributeId": "18"
 * }
 * },
 * {
 * "term": {
 * "attributeValueIndexList.valueId": "38"
 * }
 * }
 * ]
 * }
 * }
 * }
 * }
 * }
 * note: 排序 sort和 query并列
 * -----------------------------------
 * "sort": [
 * {
 * "hotScore": {
 * "order": "desc"
 * }
 * }
 * ]
 */